/***********************************************************************************************
 * Copyright (c) Microsoft Corporation All rights reserved.
 * 
 * MIT License:
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 ***********************************************************************************************/

package com.microsoft.gittf.core.tasks;

import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Calendar;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.NameConflictTreeWalk;
import org.eclipse.jgit.treewalk.TreeWalk;

import com.microsoft.gittf.core.GitTFConstants;
import com.microsoft.gittf.core.Messages;
import com.microsoft.gittf.core.config.ChangesetCommitMap;
import com.microsoft.gittf.core.interfaces.VersionControlService;
import com.microsoft.gittf.core.tasks.framework.TaskProgressDisplay;
import com.microsoft.gittf.core.tasks.framework.TaskProgressMonitor;
import com.microsoft.gittf.core.tasks.framework.TaskStatus;
import com.microsoft.gittf.core.util.Check;
import com.microsoft.gittf.core.util.StashUtil;
import com.microsoft.gittf.core.util.tree.CommitTreeEntry;
import com.microsoft.gittf.core.util.tree.CommitTreePath;
import com.microsoft.gittf.core.util.tree.CommitTreePathComparator;
import com.microsoft.tfs.core.clients.versioncontrol.GetItemsOptions;
import com.microsoft.tfs.core.clients.versioncontrol.PropertyConstants;
import com.microsoft.tfs.core.clients.versioncontrol.PropertyUtils;
import com.microsoft.tfs.core.clients.versioncontrol.path.ServerPath;
import com.microsoft.tfs.core.clients.versioncontrol.soapextensions.ChangeType;
import com.microsoft.tfs.core.clients.versioncontrol.soapextensions.DeletedState;
import com.microsoft.tfs.core.clients.versioncontrol.soapextensions.Item;
import com.microsoft.tfs.core.clients.versioncontrol.soapextensions.ItemType;
import com.microsoft.tfs.core.clients.versioncontrol.soapextensions.PendingChange;
import com.microsoft.tfs.core.clients.versioncontrol.soapextensions.PendingSet;
import com.microsoft.tfs.core.clients.versioncontrol.specs.version.LatestVersionSpec;
import com.microsoft.tfs.util.FileHelpers;

public abstract class CreateCommitForPendingSetsTask
    extends CreateCommitTask
{
    private static final Log log = LogFactory.getLog(CreateCommitForPendingSetsTask.class);

    private boolean createStashCommit = false;

    public CreateCommitForPendingSetsTask(
        final Repository repository,
        final VersionControlService versionControlClient,
        ObjectId parentCommitID)
    {
        super(repository, versionControlClient, parentCommitID);
    }

    public void setCreateStashCommit(boolean createStashCommit)
    {
        this.createStashCommit = createStashCommit;
    }

    public abstract String getProgressMonitorMessage();

    public abstract PendingSet[] getPendingSets();

    public abstract String getOwnerDisplayName();

    public abstract String getOwner();

    public abstract String getCommitterDisplayName();

    public abstract String getCommitter();

    public abstract Calendar getCommitDate();

    public abstract String getComment();

    public abstract String getName();

    @Override
    public TaskStatus run(final TaskProgressMonitor progressMonitor)
    {
        progressMonitor.beginTask(getProgressMonitorMessage(), 1, TaskProgressDisplay.DISPLAY_SUBTASK_DETAIL);

        ObjectInserter repositoryInserter = null;
        TreeWalk treeWalker = null;
        RevWalk walk = null;

        try
        {
            validateTempDirectory();

            Item rootServerItem =
                versionControlService.getItem(
                    serverPath,
                    LatestVersionSpec.INSTANCE,
                    DeletedState.NON_DELETED,
                    GetItemsOptions.NONE);

            String serverPathToUse = rootServerItem.getServerItem();

            Set<String> pendingSetItemPath = new TreeSet<String>();
            Map<String, PendingChange> pendingSetMap = new HashMap<String, PendingChange>();

            PendingSet[] pendingSets = getPendingSets();

            /*
             * keep track of the items added, renamed and folders renamed for
             * special handling later
             */
            Set<String> itemsAddedInPendingSet = new TreeSet<String>();
            Set<String> itemsRenamedInPendingSet = new TreeSet<String>();
            Set<String> itemsDeletedInPendingSet = new TreeSet<String>();

            Set<String> foldersRenamedInPendingSet = new TreeSet<String>(Collections.reverseOrder());
            Set<String> foldersDeletedInPendingSet = new TreeSet<String>();

            progressMonitor.displayVerbose(Messages.getString("CreateCommitForPendingSetsTask.VerboseItemsProcessedFromPendingSets")); //$NON-NLS-1$

            for (PendingSet set : pendingSets)
            {
                for (PendingChange change : set.getPendingChanges())
                {
                    String serverItem = change.getServerItem();
                    String sourceServerItem =
                        change.getSourceServerItem() != null ? change.getSourceServerItem() : null;

                    String pathToUse = serverItem;

                    ChangeType changeType = change.getChangeType();

                    if (change.getItemType() == ItemType.FILE)
                    {
                        if (changeType.contains(ChangeType.ADD)
                            || changeType.contains(ChangeType.BRANCH)
                            || changeType.contains(ChangeType.UNDELETE))
                        {
                            itemsAddedInPendingSet.add(serverItem);
                        }
                        else if (changeType.contains(ChangeType.RENAME))
                        {
                            itemsRenamedInPendingSet.add(sourceServerItem);

                            pathToUse = sourceServerItem;
                        }
                        else if (changeType.contains(ChangeType.DELETE))
                        {
                            itemsDeletedInPendingSet.add(serverItem);
                        }
                        else
                        {
                            /*
                             * in case there is a source server item use that.
                             * This will be true in the case of a file edit and
                             * its parent has been renamed
                             */
                            if (change.getSourceServerItem() != null)
                            {
                                pathToUse = sourceServerItem;
                            }
                        }
                    }
                    else if (change.getItemType() == ItemType.FOLDER)
                    {
                        if (changeType.contains(ChangeType.RENAME))
                        {
                            foldersRenamedInPendingSet.add(sourceServerItem);

                            pathToUse = sourceServerItem;
                        }
                        else if (changeType.contains(ChangeType.DELETE))
                        {
                            foldersDeletedInPendingSet.add(serverItem);
                        }
                    }

                    progressMonitor.displayVerbose(pathToUse);

                    pendingSetItemPath.add(pathToUse);
                    pendingSetMap.put(pathToUse, change);
                }
            }

            progressMonitor.displayVerbose(""); //$NON-NLS-1$

            progressMonitor.setWork(pendingSetItemPath.size());

            repositoryInserter = repository.newObjectInserter();
            treeWalker = new NameConflictTreeWalk(repository);
            walk = new RevWalk(repository);

            ObjectId baseCommitId = parentCommitID;
            if (baseCommitId == null)
            {
                ChangesetCommitMap commitMap = new ChangesetCommitMap(repository);
                baseCommitId = commitMap.getCommitID(commitMap.getLastBridgedChangesetID(true), false);
            }

            RevCommit parentCommit = walk.parseCommit(baseCommitId);
            if (parentCommit == null)
            {
                throw new Exception(
                    Messages.getString("CreateCommitForPendingSetsTask.LatestDownloadedChangesetNotFound")); //$NON-NLS-1$
            }

            RevTree baseCommitTree = parentCommit.getTree();

            /*
             * We want trees sorted by children first so we can simply walk them
             * (child-first) to build the hierarchy once we've finished
             * inserting blobs.
             */

            final Map<CommitTreePath, Map<CommitTreePath, CommitTreeEntry>> baseTreeHeirarchy =
                new TreeMap<CommitTreePath, Map<CommitTreePath, CommitTreeEntry>>(new CommitTreePathComparator());

            final Map<CommitTreePath, Map<CommitTreePath, CommitTreeEntry>> pendingSetTreeHeirarchy =
                new TreeMap<CommitTreePath, Map<CommitTreePath, CommitTreeEntry>>(new CommitTreePathComparator());

            treeWalker.setRecursive(true);
            treeWalker.addTree(baseCommitTree);

            /*
             * Phase one: build the pending set commit tree by copying the
             * parent tree
             */

            progressMonitor.displayVerbose(Messages.getString("CreateCommitForPendingSetsTask.VerboseItemsDownloadedFromPendingSets")); //$NON-NLS-1$

            while (treeWalker.next())
            {
                String itemServerPath = ServerPath.combine(serverPathToUse, treeWalker.getPathString());

                /* if the item has a pending change apply the pending change */
                if (pendingSetItemPath.contains(itemServerPath))
                {
                    progressMonitor.displayVerbose(itemServerPath);

                    if (createStashCommit)
                    {
                        createBlob(
                            repositoryInserter,
                            baseTreeHeirarchy,
                            pendingSetMap.get(itemServerPath),
                            true,
                            progressMonitor);
                    }

                    if (!itemsDeletedInPendingSet.contains(itemServerPath)
                        && !itemsRenamedInPendingSet.contains(itemServerPath))
                    {
                        createBlob(
                            repositoryInserter,
                            pendingSetTreeHeirarchy,
                            pendingSetMap.get(itemServerPath),
                            false,
                            progressMonitor);
                    }

                    progressMonitor.worked(1);
                }
                /* if the item parent is renamed handle this case */
                else if (isParentInCollection(foldersRenamedInPendingSet, itemServerPath))
                {
                    if (createStashCommit)
                    {
                        createBlob(
                            repositoryInserter,
                            baseTreeHeirarchy,
                            itemServerPath,
                            treeWalker.getObjectId(0),
                            treeWalker.getFileMode(0),
                            progressMonitor);
                    }

                    String destinationServerItem =
                        updateServerItemWithParentRename(foldersRenamedInPendingSet, itemServerPath, pendingSetMap);
                    if (ServerPath.isChild(serverPathToUse, destinationServerItem))
                    {
                        createBlob(
                            repositoryInserter,
                            pendingSetTreeHeirarchy,
                            destinationServerItem,
                            treeWalker.getObjectId(0),
                            treeWalker.getFileMode(0),
                            progressMonitor);
                    }
                }
                /*
                 * add all other items to the tree unless their parent was
                 * deleted
                 */
                else
                {
                    if (createStashCommit)
                    {
                        createBlob(
                            repositoryInserter,
                            baseTreeHeirarchy,
                            itemServerPath,
                            treeWalker.getObjectId(0),
                            treeWalker.getFileMode(0),
                            progressMonitor);
                    }

                    if (!isParentInCollection(foldersDeletedInPendingSet, itemServerPath))
                    {
                        createBlob(
                            repositoryInserter,
                            pendingSetTreeHeirarchy,
                            itemServerPath,
                            treeWalker.getObjectId(0),
                            treeWalker.getFileMode(0),
                            progressMonitor);
                    }
                }
            }

            progressMonitor.displayVerbose(""); //$NON-NLS-1$

            /* for items that were added in the shelveset add those here */

            progressMonitor.displayVerbose(Messages.getString("CreateCommitForPendingSetsTask.VerboseItemsDownloadedFromPendingSetsAdds")); //$NON-NLS-1$

            for (String newItem : itemsAddedInPendingSet)
            {
                if (!ServerPath.isChild(serverPathToUse, newItem))
                {
                    // Ignore files that are added that are not mapped in the
                    // repository
                    continue;
                }

                progressMonitor.displayVerbose(newItem);

                createBlob(
                    repositoryInserter,
                    pendingSetTreeHeirarchy,
                    pendingSetMap.get(newItem),
                    false,
                    progressMonitor);

                progressMonitor.worked(1);
            }

            for (String renamedItem : itemsRenamedInPendingSet)
            {
                PendingChange change = pendingSetMap.get(renamedItem);

                if (!ServerPath.isChild(serverPathToUse, change.getServerItem()))
                {
                    // Ignore files that are renamed to server items that are
                    // outside the repository
                    continue;
                }

                progressMonitor.displayVerbose(renamedItem);

                createBlob(repositoryInserter, pendingSetTreeHeirarchy, change, false, progressMonitor);

                progressMonitor.worked(1);
            }

            progressMonitor.displayVerbose(""); //$NON-NLS-1$

            /* Phase two: add child trees to their parents. */
            progressMonitor.setDetail(Messages.getString("CreateCommitTask.CreatingTrees")); //$NON-NLS-1$

            ObjectId rootBaseTree = createStashCommit ? createTrees(repositoryInserter, baseTreeHeirarchy) : null;
            ObjectId rootPendingSetTree = createTrees(repositoryInserter, pendingSetTreeHeirarchy);

            /* Phase three: create the commit. */
            progressMonitor.setDetail(Messages.getString("CreateCommitTask.CreatingCommit")); //$NON-NLS-1$

            if (createStashCommit)
            {
                this.commitId =
                    StashUtil.create(
                        repository,
                        repositoryInserter,
                        rootBaseTree,
                        rootPendingSetTree,
                        rootBaseTree,
                        parentCommitID,
                        getOwnerDisplayName(),
                        getOwner(),
                        getComment(),
                        getName());
            }
            else
            {
                this.commitId = createCommit(repositoryInserter, rootPendingSetTree, parentCommitID);
            }

            progressMonitor.endTask();

            return TaskStatus.OK_STATUS;
        }
        catch (Exception e)
        {
            log.error(e);
            return new TaskStatus(TaskStatus.ERROR, e);
        }
        finally
        {
            FileHelpers.deleteDirectory(tempDir);

            if (repositoryInserter != null)
            {
                repositoryInserter.release();
            }

            if (treeWalker != null)
            {
                treeWalker.release();
            }

            if (walk != null)
            {
                walk.release();
            }
        }
    }

    private String updateServerItemWithParentRename(
        Set<String> folderCollection,
        String serverPath,
        Map<String, PendingChange> pendingSetMap)
    {
        String parentToUpdate = getParentInCollection(folderCollection, serverPath);

        Check.notNull(parentToUpdate, "parentToUpdate"); //$NON-NLS-1$

        PendingChange pendingChange = pendingSetMap.get(parentToUpdate);

        Check.notNull(pendingChange, "pendingChange"); //$NON-NLS-1$

        String newParentName = pendingChange.getServerItem();

        return newParentName + serverPath.substring(parentToUpdate.length());
    }

    private boolean isParentInCollection(Set<String> folderCollection, String serverPath)
    {
        return getParentInCollection(folderCollection, serverPath) != null;
    }

    private String getParentInCollection(Set<String> folderCollection, String serverPath)
    {
        String currentPath = ServerPath.getParent(serverPath);
        while (currentPath != null && currentPath.length() > 0 && !currentPath.equals(ServerPath.ROOT))
        {
            if (folderCollection.contains(currentPath))
            {
                return currentPath;
            }

            currentPath = ServerPath.getParent(currentPath);
        }

        return null;
    }

    private void createBlob(
        final ObjectInserter repositoryInserter,
        final Map<CommitTreePath, Map<CommitTreePath, CommitTreeEntry>> treeHierarchy,
        final PendingChange pendingChange,
        final boolean addBaseContent,
        final TaskProgressMonitor progressMonitor)
        throws Exception
    {
        if (pendingChange.getItemType() == ItemType.FOLDER)
        {
            return;
        }

        File tempFile = null;
        InputStream tempInputStream = null;
        ObjectId blobID = null;

        try
        {
            tempFile = File.createTempFile(GitTFConstants.GIT_TF_NAME, null, tempDir);

            if (addBaseContent)
            {
                versionControlService.downloadBaseFile(pendingChange, tempFile.getAbsolutePath());
            }
            else
            {
                versionControlService.downloadShelvedFile(pendingChange, tempFile.getAbsolutePath());
            }

            if (tempFile.exists())
            {
                tempInputStream = new FileInputStream(tempFile);
                blobID = repositoryInserter.insert(OBJ_BLOB, tempFile.length(), tempInputStream);
            }
            else
            {
                blobID = ObjectId.zeroId();
            }

            FileMode fileMode;

            /* handle executable files */
            if (pendingChange.getPropertyValues() != null)
            {
                if (PropertyConstants.EXECUTABLE_ENABLED_VALUE.equals(PropertyUtils.selectMatching(
                    pendingChange.getPropertyValues(),
                    PropertyConstants.EXECUTABLE_KEY)))
                {
                    fileMode = addBaseContent ? FileMode.REGULAR_FILE : FileMode.EXECUTABLE_FILE;
                }
                else
                {
                    fileMode = addBaseContent ? FileMode.EXECUTABLE_FILE : FileMode.REGULAR_FILE;
                }
            }
            else
            {
                fileMode = FileMode.MISSING;
            }

            String serverItem =
                pendingChange.getSourceServerItem() != null && addBaseContent ? pendingChange.getSourceServerItem()
                    : pendingChange.getServerItem();

            createBlob(repositoryInserter, treeHierarchy, serverItem, blobID, fileMode, progressMonitor);
        }
        finally
        {
            if (tempInputStream != null)
            {
                tempInputStream.close();
            }

            if (tempFile != null)
            {
                tempFile.delete();
            }
        }
    }

    private ObjectId createCommit(ObjectInserter repositoryInserter, ObjectId rootPendingSetTree, ObjectId parentId)
        throws IOException
    {
        Check.notNull(repositoryInserter, "repositoryInserter"); //$NON-NLS-1$
        Check.notNull(rootPendingSetTree, "rootTree"); //$NON-NLS-1$

        return createCommit(
            repositoryInserter,
            rootPendingSetTree,
            parentId,
            getOwnerDisplayName(),
            getOwner(),
            getCommitterDisplayName(),
            getCommitter(),
            getCommitDate(),
            getComment());
    }
}
